﻿#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QCoreApplication>

#include <QImage>
#include <QPixmap>
#include <QMatrix>
#include <QVector>
#include <QTimer>
#include <QFile>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <Saba/Base/Path.h>
#include <Saba/Base/File.h>
#include <Saba/Base/UnicodeUtil.h>
#include <Saba/Base/Time.h>
#include <Saba/Model/MMD/PMDModel.h>
#include <Saba/Model/MMD/PMXModel.h>
#include <Saba/Model/MMD/VMDFile.h>
#include <Saba/Model/MMD/VMDAnimation.h>
#include <Saba/Model/MMD/VMDCameraAnimation.h>

QMap<QString,QOpenGLTexture *> textures;

glm::mat4	m_viewMat;
glm::mat4	m_projMat;

QOpenGLTexture * GetTexture(QString filepath){
    if(textures.find(filepath) != textures.end())return textures[filepath];
    QOpenGLTexture * texture = new QOpenGLTexture(QImage(filepath).mirrored());
    if(texture->textureId() == 0)qDebug()<<filepath;
    texture->setMinificationFilter(QOpenGLTexture::Linear);
    texture->setMagnificationFilter(QOpenGLTexture::Linear);
    texture->setWrapMode(QOpenGLTexture::Repeat);
    textures[filepath] = texture;
    return texture;
}

class MMDShader{
public:
    explicit MMDShader(QString vertexFile,QString fragFile){
        mmdShader.addShaderFromSourceFile(QOpenGLShader::Vertex,vertexFile);
        mmdShader.addShaderFromSourceFile(QOpenGLShader::Fragment,fragFile);
        mmdShader.link();
        mmdShader.bind();

        // attribute
        m_inPos = mmdShader.attributeLocation("in_Pos");
        m_inNor = mmdShader.attributeLocation("in_Nor");
        m_inUV = mmdShader.attributeLocation("in_UV");

        // uniform
        m_uWV = mmdShader.uniformLocation("u_WV");
        m_uWVP = mmdShader.uniformLocation("u_WVP");

        m_uAmbinet = mmdShader.uniformLocation("u_Ambient");
        m_uDiffuse = mmdShader.uniformLocation("u_Diffuse");
        m_uSpecular = mmdShader.uniformLocation("u_Specular");
        m_uSpecularPower = mmdShader.uniformLocation("u_SpecularPower");
        m_uAlpha = mmdShader.uniformLocation("u_Alpha");

        m_uTexMode = mmdShader.uniformLocation("u_TexMode");
        m_uTex = mmdShader.uniformLocation("u_Tex");
        m_uTexMulFactor = mmdShader.uniformLocation("u_TexMulFactor");
        m_uTexAddFactor = mmdShader.uniformLocation("u_TexAddFactor");

        m_uSphereTexMode = mmdShader.uniformLocation("u_SphereTexMode");
        m_uSphereTex = mmdShader.uniformLocation("u_SphereTex");
        m_uSphereTexMulFactor = mmdShader.uniformLocation("u_SphereTexMulFactor");
        m_uSphereTexAddFactor = mmdShader.uniformLocation("u_SphereTexAddFactor");

        m_uToonTexMode = mmdShader.uniformLocation("u_ToonTexMode");
        m_uToonTex = mmdShader.uniformLocation("u_ToonTex");
        m_uToonTexMulFactor = mmdShader.uniformLocation("u_ToonTexMulFactor");
        m_uToonTexAddFactor = mmdShader.uniformLocation("u_ToonTexAddFactor");

        m_uLightColor = mmdShader.uniformLocation("u_LightColor");
        m_uLightDir = mmdShader.uniformLocation("u_LightDir");

        m_uLightVP = mmdShader.uniformLocation("u_LightWVP");
    }
    QOpenGLShaderProgram & get(){
        return mmdShader;
    }
public:
    // attribute
    GLint	m_inPos = -1;
    GLint	m_inNor = -1;
    GLint	m_inUV = -1;

    // uniform
    GLint	m_uWV = -1;
    GLint	m_uWVP = -1;

    GLint	m_uAmbinet = -1;
    GLint	m_uDiffuse = -1;
    GLint	m_uSpecular = -1;
    GLint	m_uSpecularPower = -1;
    GLint	m_uAlpha = -1;

    GLint	m_uTexMode = -1;
    GLint	m_uTex = -1;
    GLint	m_uTexMulFactor = -1;
    GLint	m_uTexAddFactor = -1;

    GLint	m_uSphereTexMode = -1;
    GLint	m_uSphereTex = -1;
    GLint	m_uSphereTexMulFactor = -1;
    GLint	m_uSphereTexAddFactor = -1;

    GLint	m_uToonTexMode = -1;
    GLint	m_uToonTex = -1;
    GLint	m_uToonTexMulFactor = -1;
    GLint	m_uToonTexAddFactor = -1;

    GLint	m_uLightColor = -1;
    GLint	m_uLightDir = -1;

    GLint	m_uLightVP = -1;
private:
    QOpenGLShaderProgram mmdShader;
};

class Material{
public:
    explicit Material(saba::MMDMaterial mat)
        : m_mmdMat(mat)
    {}
public:
    saba::MMDMaterial m_mmdMat;
    QOpenGLTexture *m_texture;
    //bool	m_textureHasAlpha = false;
    QOpenGLTexture *m_spTexture;
    QOpenGLTexture *m_toonTexture;
private:

};

class Model : protected QOpenGLFunctions{
public:
    explicit Model(QString modelFile,QString MMDPath,MMDShader * shader):
        m_posVBO(QOpenGLBuffer::VertexBuffer),
        m_norVBO(QOpenGLBuffer::VertexBuffer),
        m_uvVBO(QOpenGLBuffer::VertexBuffer),
        m_ibo(QOpenGLBuffer::IndexBuffer)
    {
        initializeOpenGLFunctions();

        this->shader = shader;
        QOpenGLShaderProgram & program = shader->get();
        program.bind();

        glGenTextures(1, &m_dummyColorTex);
        glBindTexture(GL_TEXTURE_2D, m_dummyColorTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glBindTexture(GL_TEXTURE_2D, 0);

        glGenTextures(1, &m_dummyShadowDepthTex);
        glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 1, 1, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
        glBindTexture(GL_TEXTURE_2D, 0);

        model.Load(modelFile.toStdString(),MMDPath.toStdString());
        model.InitializeAnimation();

        m_mmdVAO.create();
        m_mmdVAO.bind();

        // Setup vertices
        size_t vtxCount = model.GetVertexCount();

        m_posVBO.create();
        m_posVBO.bind();
        m_posVBO.setUsagePattern(QOpenGLBuffer::UsagePattern::DynamicDraw);
        m_posVBO.allocate(sizeof(glm::vec3) * vtxCount);
        program.enableAttributeArray(shader->m_inPos);
        program.setAttributeBuffer(shader->m_inPos,GL_FLOAT,0,3,sizeof(glm::vec3));
        m_posVBO.release();

        m_norVBO.create();
        m_norVBO.bind();
        m_norVBO.setUsagePattern(QOpenGLBuffer::UsagePattern::DynamicDraw);
        m_norVBO.allocate(sizeof(glm::vec3) * vtxCount);
        program.enableAttributeArray(shader->m_inNor);
        program.setAttributeBuffer(shader->m_inNor,GL_FLOAT,0,3,sizeof(glm::vec3));
        m_norVBO.release();

        m_uvVBO.create();
        m_uvVBO.bind();
        m_uvVBO.setUsagePattern(QOpenGLBuffer::UsagePattern::DynamicDraw);
        m_uvVBO.allocate(sizeof(glm::vec2) * vtxCount);
        program.enableAttributeArray(shader->m_inUV);
        program.setAttributeBuffer(shader->m_inUV,GL_FLOAT,0,2,sizeof(glm::vec2));
        m_uvVBO.release();

        size_t idxSize = model.GetIndexElementSize();
        size_t idxCount = model.GetIndexCount();
        m_ibo.create();
        m_ibo.bind();
        m_ibo.setUsagePattern(QOpenGLBuffer::UsagePattern::StaticDraw);
        m_ibo.allocate(model.GetIndices(),idxSize * idxCount);

        m_mmdVAO.release();
        program.release();

        if (idxSize == 1)
        {
            m_indexType = GL_UNSIGNED_BYTE;
        }
        else if (idxSize == 2)
        {
            m_indexType = GL_UNSIGNED_SHORT;
        }
        else if (idxSize == 4)
        {
            m_indexType = GL_UNSIGNED_INT;
        }

        for (size_t i = 0; i < model.GetMaterialCount(); i++)
        {
            auto mmdMat = model.GetMaterials()[i];
            Material mat(mmdMat);
            if (!mmdMat.m_texture.empty())
            {
                mat.m_texture = GetTexture(QString::fromStdString(mmdMat.m_texture));
            }
            if (!mmdMat.m_spTexture.empty())
            {
                mat.m_spTexture = GetTexture(QString::fromStdString(mmdMat.m_spTexture));
            }
            if (!mmdMat.m_toonTexture.empty())
            {
                mat.m_toonTexture = GetTexture(QString::fromStdString(mmdMat.m_toonTexture));
            }
            m_materials.push_back(mat);
        }
        std::shared_ptr<saba::MMDModel> tmp(&model);
        anim.Create(tmp);
    }
    void Draw(){
        const auto& view = m_viewMat;
        const auto& proj = m_projMat;

        auto world = glm::mat4(1.0f);
        auto wv = view * world;
        auto wvp = proj * view * world;
        auto wvit = glm::mat3(view * world);
        wvit = glm::inverse(wvit);
        wvit = glm::transpose(wvit);

        glActiveTexture(GL_TEXTURE0 + 3);
        glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);
        glActiveTexture(GL_TEXTURE0 + 4);
        glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);
        glActiveTexture(GL_TEXTURE0 + 5);
        glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);
        glActiveTexture(GL_TEXTURE0 + 6);
        glBindTexture(GL_TEXTURE_2D, m_dummyShadowDepthTex);

        glEnable(GL_DEPTH_TEST);

        // Draw model
        size_t subMeshCount = model.GetSubMeshCount();
        for (size_t i = 0; i < subMeshCount; i++)
        {
            const auto& subMesh = model.GetSubMeshes()[i];
            const auto& mat = m_materials[subMesh.m_materialID];
            const auto& mmdMat = mat.m_mmdMat;
            QOpenGLShaderProgram & program = shader->get();

            if (mat.m_mmdMat.m_alpha == 0)continue;

            program.bind();
            m_mmdVAO.bind();

            GLfloat value[4][4];
            memcpy(&value,&wv[0][0],sizeof(value));
            program.setUniformValue(shader->m_uWV,value);
            memcpy(&value,&wvp[0][0],sizeof(value));
            program.setUniformValue(shader->m_uWVP,value);

            bool alphaBlend = true;
            program.setUniformValue(shader->m_uAmbinet,QVector3D(mmdMat.m_ambient.r,mmdMat.m_ambient.g,mmdMat.m_ambient.b));
            program.setUniformValue(shader->m_uDiffuse,QVector3D(mmdMat.m_diffuse.r,mmdMat.m_diffuse.g,mmdMat.m_diffuse.b));
            program.setUniformValue(shader->m_uSpecular,QVector3D(mmdMat.m_specular.r,mmdMat.m_specular.g,mmdMat.m_specular.b));
            program.setUniformValue(shader->m_uSpecularPower,mmdMat.m_specularPower);
            program.setUniformValue(shader->m_uAlpha,mmdMat.m_alpha);

            glActiveTexture(GL_TEXTURE0 + 0);
            program.setUniformValue(shader->m_uTex,0);
            if (mat.m_texture != 0)
            {
                if (mat.m_texture->format() != QOpenGLTexture::RGBA8_UNorm)
                {
                    // Use Material Alpha
                    program.setUniformValue(shader->m_uTexMode,1);
                }
                else
                {
                    // Use Material Alpha * Texture Alpha
                    program.setUniformValue(shader->m_uTexMode,2);
                }
                program.setUniformValue(shader->m_uTexMulFactor,QVector4D(mmdMat.m_textureMulFactor.r,mmdMat.m_textureMulFactor.g,mmdMat.m_textureMulFactor.b,mmdMat.m_textureMulFactor.a));
                program.setUniformValue(shader->m_uTexAddFactor,QVector4D(mmdMat.m_textureAddFactor.r,mmdMat.m_textureAddFactor.g,mmdMat.m_textureAddFactor.b,mmdMat.m_textureAddFactor.a));
                mat.m_texture->bind();
            }
            else
            {
                program.setUniformValue(shader->m_uTexMode,0);
                glBindTexture(GL_TEXTURE_2D, m_dummyColorTex);
            }

            glActiveTexture(GL_TEXTURE0 + 1);
            program.setUniformValue(shader->m_uSphereTex,1);
            if (mat.m_spTexture->isCreated())
            {
                if (mmdMat.m_spTextureMode == saba::MMDMaterial::SphereTextureMode::Mul)
                {
                    program.setUniformValue(shader->m_uSphereTexMode,1);
                }
                else if (mmdMat.m_spTextureMode == saba::MMDMaterial::SphereTextureMode::Add)
                {
                    program.setUniformValue(shader->m_uSphereTexMode, 2);
                }
                program.setUniformValue(shader->m_uSphereTexMulFactor,QVector4D(mmdMat.m_spTextureMulFactor.r,mmdMat.m_spTextureMulFactor.g,mmdMat.m_spTextureMulFactor.b,mmdMat.m_spTextureMulFactor.a));
                program.setUniformValue(shader->m_uSphereTexAddFactor,QVector4D(mmdMat.m_spTextureAddFactor.r,mmdMat.m_spTextureAddFactor.g,mmdMat.m_spTextureAddFactor.b,mmdMat.m_spTextureAddFactor.a));
                mat.m_spTexture->bind();
            }
            else
            {
                program.setUniformValue(shader->m_uSphereTexMode, 0);
                glBindTexture(GL_TEXTURE_2D, m_dummyColorTex);
            }

            glActiveTexture(GL_TEXTURE0 + 2);
            program.setUniformValue(shader->m_uToonTex, 2);
            if (mat.m_toonTexture->isCreated())
            {
                program.setUniformValue(shader->m_uToonTexMulFactor,QVector4D(mmdMat.m_toonTextureMulFactor.r,mmdMat.m_toonTextureMulFactor.g,mmdMat.m_toonTextureMulFactor.b,mmdMat.m_toonTextureMulFactor.a));
                program.setUniformValue(shader->m_uToonTexAddFactor,QVector4D(mmdMat.m_toonTextureAddFactor.r,mmdMat.m_toonTextureAddFactor.g,mmdMat.m_toonTextureAddFactor.b,mmdMat.m_toonTextureAddFactor.a));
                program.setUniformValue(shader->m_uToonTexMode, 1);
                mat.m_toonTexture->bind();
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            }
            else
            {
                program.setUniformValue(shader->m_uToonTexMode, 0);
                glBindTexture(GL_TEXTURE_2D, m_dummyColorTex);
            }

            glm::vec3 lightColor = m_lightColor;
            glm::vec3 lightDir = m_lightDir;
            glm::mat3 viewMat = glm::mat3(m_viewMat);
            lightDir = viewMat * lightDir;
            program.setUniformValue(shader->m_uLightDir,lightDir.x,lightDir.y,lightDir.z);
            program.setUniformValue(shader->m_uLightColor,lightColor.x,lightColor.y,lightColor.z);

            if (mmdMat.m_bothFace)
            {
                glDisable(GL_CULL_FACE);
            }
            else
            {
                glEnable(GL_CULL_FACE);
                glCullFace(GL_BACK);
            }

            if (alphaBlend)
            {
                glEnable(GL_BLEND);
                glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            }
            else
            {
                glDisable(GL_BLEND);
            }

            size_t offset = subMesh.m_beginIndex * model.GetIndexElementSize();
            glDrawElements(GL_TRIANGLES, subMesh.m_vertexCount, m_indexType, (GLvoid*)offset);

            glActiveTexture(GL_TEXTURE0 + 2);
            glBindTexture(GL_TEXTURE_2D, 0);
            glActiveTexture(GL_TEXTURE0 + 1);
            glBindTexture(GL_TEXTURE_2D, 0);
            glActiveTexture(GL_TEXTURE0 + 0);
            glBindTexture(GL_TEXTURE_2D, 0);
        }

        glActiveTexture(GL_TEXTURE0 + 3);
        glBindTexture(GL_TEXTURE_2D, 0);
        glActiveTexture(GL_TEXTURE0 + 4);
        glBindTexture(GL_TEXTURE_2D, 0);
        glActiveTexture(GL_TEXTURE0 + 5);
        glBindTexture(GL_TEXTURE_2D, 0);
        glActiveTexture(GL_TEXTURE0 + 6);
        glBindTexture(GL_TEXTURE_2D, 0);

        glDisable(GL_POLYGON_OFFSET_FILL);
        glDisable(GL_STENCIL_TEST);
        glDisable(GL_BLEND);
    }
    void Update(int frames){
        double time = saba::GetTime();
        double elapsed = time - saveTime;
        saveTime = time;
        if (elapsed > 1.0 / 60.0)
        {
            elapsed = 1.0 / 60.0;
        }
        passtime += elapsed;

        model.BeginAnimation();
        //(int)(passtime * 30.0)

#ifdef Q_OS_ANDROID

#else
        frames /=2;
#endif

        model.UpdateAllAnimation(&anim,frames%anim.GetMaxKeyTime(),elapsed);
        model.EndAnimation();

        model.Update();
        size_t vtxCount = model.GetVertexCount();
        m_posVBO.bind();
        m_posVBO.allocate(model.GetUpdatePositions(),sizeof(glm::vec3) * vtxCount);
        m_norVBO.bind();
        m_norVBO.allocate(model.GetUpdateNormals(),sizeof(glm::vec3) * vtxCount);
        m_uvVBO.bind();
        m_uvVBO.allocate(model.GetUpdateUVs(),sizeof(glm::vec2) * vtxCount);
    }
public:
    QOpenGLBuffer m_posVBO;
    QOpenGLBuffer m_norVBO;
    QOpenGLBuffer m_uvVBO;
    QOpenGLBuffer m_ibo;
    QOpenGLVertexArrayObject m_mmdVAO;
    //QOpenGLVertexArrayObject m_mmdEdgeVAO;
    //QOpenGLVertexArrayObject m_mmdGroundShadowVAO;
    GLenum m_indexType;
private:
    saba::PMXModel model;
    QList<Material> m_materials;
    MMDShader * shader;
private:
    GLuint	m_dummyColorTex = 0;
    GLuint	m_dummyShadowDepthTex = 0;
    glm::vec3	m_lightColor = glm::vec3(1, 1, 1);
    glm::vec3	m_lightDir = glm::vec3(-0.5f, -1.0f, -0.5f);
public:
    saba::VMDAnimation anim;
    double saveTime = saba::GetTime();
    double passtime = 0;
};

class MyGLWidget : public QOpenGLWidget ,protected QOpenGLFunctions{
public:
    explicit MyGLWidget(QWidget * parent = nullptr) : QOpenGLWidget (parent){
        QTimer *timer = new QTimer(this);
        connect(timer,SIGNAL(timeout()),this,SLOT(update()));
        timer->start(1000.0/FPS);
    }
    virtual ~MyGLWidget(){}
private:
    int frames = 0;
    const static unsigned int FPS = 60;
    QString resourceDir;
    //assets:
private:
    MMDShader *shader;
    Model * model;
    saba::VMDFile vmd;
protected:
    void initializeGL(){
        initializeOpenGLFunctions();
        //glEnable(GL_MULTISAMPLE);

#ifdef Q_OS_ANDROID
        resourceDir = "/storage/emulated/0/resource/";
        glClearColor(1.0, 1.0, 1.0, 1.0);
#else
        resourceDir = "../resource/";
        glClearColor(0.0, 0.0, 0.0, 0.0);
#endif

        m_viewMat = glm::lookAt(glm::vec3(0, 10, 50), glm::vec3(0, 10, 0), glm::vec3(0, 1, 0));
        m_projMat = glm::perspectiveFovRH(glm::radians(30.0f), float(width()), float(height()), 1.0f, 10000.0f);

        shader = new MMDShader(resourceDir+"shader/mmd.vert",resourceDir+"shader/mmd.frag");
        model = new Model(resourceDir+"model/rem/rem.pmx",resourceDir+"mmd",shader);
        saba::ReadVMDFile(&vmd,(resourceDir.toStdString()+"motion/mei_greeting.vmd").c_str());
        model->anim.Add(vmd);
        model->anim.SyncPhysics(0.0f);
    }
    void paintGL(){
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        model->Update(frames);
        model->Draw();

        frames++;
    }
    void resizeGL(int width, int height){
        glViewport(0,0,width,height);
        m_viewMat = glm::lookAt(glm::vec3(0, 10, 50), glm::vec3(0, 10, 0), glm::vec3(0, 1, 0));
        m_projMat = glm::perspectiveFovRH(glm::radians(30.0f), float(width), float(height), 1.0f, 10000.0f);
        repaint();
    }
};

#endif // MYGLWIDGET_H
